/*
 * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.truffle.api.bytecode;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;

import com.oracle.truffle.api.bytecode.InstructionDescriptor.ArgumentDescriptor;
import com.oracle.truffle.api.dsl.Bind.DefaultExpression;
import com.oracle.truffle.api.dsl.Introspection;
import com.oracle.truffle.api.dsl.Introspection.SpecializationInfo;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.api.strings.TruffleString;

/**
 * Represents metadata for an instruction in a bytecode node.
 * <p>
 * Compatibility note: The data contained in instruction classes is subject to change without notice
 * between Truffle versions. This introspection API is therefore intended to be used for debugging
 * and tracing purposes only. Do not rely on instructions for your language semantics.
 * <p>
 * The current instruction can be bound using <code>@Bind Instruction instruction</code> from
 * {@link Operation operations}. This class is not intended to be subclassed by clients, only by
 * code generated by the Bytecode DSL.
 *
 * @see BytecodeNode#getInstructions()
 * @see BytecodeNode#getInstructionsAsList()
 * @see BytecodeNode#getInstruction(int)
 * @since 24.2
 */
@DefaultExpression("$bytecodeNode.getInstruction($bytecodeIndex)")
public abstract class Instruction {

    /**
     * Internal constructor for generated code. Do not use.
     *
     * @since 24.2
     */
    protected Instruction(Object token) {
        BytecodeRootNodes.checkToken(token);
    }

    /**
     * Returns the bytecode node this instruction belongs to.
     *
     * @since 24.2
     */
    public abstract BytecodeNode getBytecodeNode();

    /**
     * Returns the bytecode index of this instruction. A bytecode index is only valid for a given
     * {@link BytecodeNode}, it is therefore recommended to use {@link #getLocation()} instead
     * whenever possible.
     *
     * @see #getLocation()
     * @since 24.2
     */
    public abstract int getBytecodeIndex();

    /**
     * Returns the descriptor object for this instruction.
     * <p>
     * The descriptor provides static metadata about the instruction executed at this bytecode
     * index, such as its name, operation code, length in bytes, argument layout, and whether it is
     * considered instrumentation.
     *
     * @see BytecodeDescriptor#getInstructionDescriptors()
     * @since 25.1
     */
    public InstructionDescriptor getDescriptor() {
        return null;
    }

    /**
     * Returns the length of this instruction in bytes.
     *
     * @see #getDescriptor()
     * @see InstructionDescriptor#getLength()
     * @since 24.2
     */
    public int getLength() {
        return getDescriptor().getLength();
    }

    /**
     * Converts this instruction pointer into a bytecode location. It is recommended to use
     * {@link BytecodeLocation} to persist a bytecode location instead of using instruction.
     *
     * @since 24.2
     */
    public final BytecodeLocation getLocation() {
        return getBytecodeNode().getBytecodeLocation(getBytecodeIndex());
    }

    /**
     * Returns the name of this instruction. The name of the instruction is purely for human
     * consumption and no structure should be assumed. If two instructions have the same instruction
     * name then they also have the same {@link #getOperationCode() operation code}.
     * <p>
     * The name is fixed for an {@link Instruction} object, but the name of an instruction at a
     * particular {@link BytecodeLocation location} can change during execution due to quickening
     * and other optimizations.
     *
     * @see #getOperationCode()
     * @see #getDescriptor()
     * @see InstructionDescriptor#getName()
     * @since 24.2
     */
    public String getName() {
        return getDescriptor().getName();
    }

    /**
     * Returns the operation code of this instruction. The operation code of the instruction is
     * purely for human consumption and no values should be assumed. If two instructions have the
     * same instruction operation code then they also have the same {@link #getName() name}.
     * <p>
     * The operation code is fixed for an {@link Instruction} object, but the operation code of an
     * instruction at a particular {@link BytecodeLocation location} can change during execution due
     * to quickening and other optimizations.
     *
     * @see #getName()
     * @see #getDescriptor()
     * @see InstructionDescriptor#getOperationCode()
     * @since 24.2
     */
    public int getOperationCode() {
        return getDescriptor().getOperationCode();
    }

    /**
     * Returns an immutable list of immediate arguments for this instructions. The number and kinds
     * of arguments remain stable during execution, however, the actual values of the arguments
     * (e.g., {@link Argument#asBranchProfile}) may change during execution.
     *
     * @since 24.2
     */
    public abstract List<Argument> getArguments();

    /**
     * Returns <code>true</code> if this instruction represents a bytecode or tag instrumentation
     * instruction, else <code>false</code>. Instrumentation instructions may get inserted
     * dynamically during execution, e.g., if a tag is materialized or an {@link Instrumentation} is
     * {@link BytecodeRootNodes#update(BytecodeConfig) configured}.
     *
     * @since 24.2
     */
    public boolean isInstrumentation() {
        return getDescriptor().isInstrumentation();
    }

    /**
     * Returns the most concrete source section associated with this instruction. If no source
     * section is available for this instruction or source sections have not yet been materialized,
     * then <code>null</code> is returned. Source sections may be materialized by calling
     * {@link BytecodeRootNodes#update(BytecodeConfig) update} with
     * {@link BytecodeConfig#WITH_SOURCE}.
     *
     * @since 24.2
     */
    public final SourceSection getSourceSection() {
        BytecodeNode bytecode = getBytecodeNode();
        if (bytecode == null || bytecode.getSourceInformation() == null) {
            // avoid materialization of source info
            return null;
        }
        return bytecode.getSourceLocation(getBytecodeIndex());
    }

    /**
     * Returns all source section associated with this instruction starting with the most concrete
     * source section. If no source sections are available, then an empty array is returned. If
     * source sections have not yet been materialized, then <code>null</code> is returned. Source
     * sections may be materialized by calling {@link BytecodeRootNodes#update(BytecodeConfig)
     * update} with {@link BytecodeConfig#WITH_SOURCE}.
     *
     * @since 24.2
     */
    public final SourceSection[] getSourceSections() {
        BytecodeNode bytecode = getBytecodeNode();
        if (bytecode == null || bytecode.getSourceInformation() == null) {
            // avoid materialization of source info
            return null;
        }
        return getBytecodeNode().getSourceLocations(getBytecodeIndex());
    }

    /**
     * Returns the bytecode index of the next instruction. This method is useful to quickly find the
     * next instruction. The next bytecode index is computed as <code>{@link #getBytecodeIndex()} +
     * {@link #getLength()}</code>.
     * <p>
     * The next bytecode index may not be a valid index if this instruction is the last instruction.
     * Use {@link BytecodeNode#getInstructions()} to walk all instructions efficiently and safely.
     * Since the bytecode encoding is variable length, there is no efficient way to get to the
     * previous bytecode index. Only forward traversial is efficient. If random access is desired
     * use {@link BytecodeNode#getInstructionsAsList()}.
     *
     * @since 24.2
     */
    public final int getNextBytecodeIndex() {
        return getBytecodeIndex() + getLength();
    }

    /**
     * Returns the next instruction object. Implemented by generated code, intended for internal use
     * only.
     *
     * @since 24.2
     */
    protected abstract Instruction next();

    /**
     * {@inheritDoc}
     *
     * @since 24.2
     */
    @Override
    public final int hashCode() {
        return Objects.hash(getBytecodeNode(), getBytecodeIndex(), getOperationCode());
    }

    /**
     * {@inheritDoc}
     *
     * @since 24.2
     */
    @Override
    public final boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof Instruction other) {
            return getBytecodeNode() == other.getBytecodeNode() && getBytecodeIndex() == other.getBytecodeIndex() && getOperationCode() == other.getOperationCode();
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @since 24.2
     */
    @Override
    public final String toString() {
        return formatInstruction(null, -1, this, 40, 120);
    }

    static String formatInstruction(List<Throwable> errors, int index, Instruction instruction, int maxLabelWidth, int maxArgumentWidth) {
        StringBuilder sb = new StringBuilder();
        if (index != -1) {
            sb.append(String.format("%3d ", index));
        }
        String label = formatLabel(instruction);
        sb.append(label);
        appendSpaces(sb, maxLabelWidth - label.length());
        String arguments = formatArguments(instruction);
        sb.append(arguments);
        try {
            SourceSection s = instruction.getSourceSection();
            if (s != null) {
                appendSpaces(sb, maxArgumentWidth - arguments.length());
                sb.append(" | ");
                sb.append(SourceInformation.formatSourceSection(s, 60));
            }
        } catch (Throwable t) {
            if (errors != null) {
                errors.add(t);
                sb.append(" | " + t.toString());
            } else {
                throw t;
            }
        }
        return sb.toString();
    }

    private static void appendSpaces(StringBuilder sb, int spaces) {
        for (int i = 0; i < spaces; i++) {
            sb.append(' ');
        }
    }

    static String formatLabel(Instruction instruction) {
        return String.format("[%03x] %03x %s", instruction.getBytecodeIndex(), instruction.getOperationCode(), instruction.getName());
    }

    static String formatArguments(Instruction instruction) {
        StringBuilder b = new StringBuilder(" ");
        for (Argument a : instruction.getArguments()) {
            b.append(' ').append(a.toString());
        }
        return b.toString();
    }

    /**
     * Represents metadata for an argument of an instruction in a bytecode node.
     * <p>
     * Compatibility note: The data contained in instruction classes is subject to change without
     * notice between Truffle versions. This introspection API is therefore intended to be used for
     * debugging and tracing purposes only. Do not rely on instructions for your language semantics.
     *
     * @see Instruction#getArguments()
     * @since 24.2
     */
    public abstract static class Argument {

        /**
         * Internal constructor for generated code. Do not use.
         *
         * @since 24.2
         */
        protected Argument(Object token) {
            BytecodeRootNodes.checkToken(token);
        }

        /**
         * Returns the descriptor object for this argument.
         * <p>
         * The descriptor provides static metadata about the argument position and interpretation
         * for this instruction, including its {@link Kind}, printable name, and encoded width in
         * bytes.
         *
         * @since 25.1
         */
        public ArgumentDescriptor getDescriptor() {
            return null;
        }

        /**
         * Returns the {@link Kind} of this argument. The kind determines which {@code asX} method
         * can be called on a given argument.
         * <p>
         * For example, if the kind is {@link Kind#INTEGER}, the {@link #asInteger} method will
         * return the integer value. All other {@code asX} methods (e.g., {@link #asBranchProfile})
         * will throw an exception.
         *
         * @since 24.2
         */
        public Kind getKind() {
            return getDescriptor().getKind();
        }

        /**
         * Returns a human readable name for this argument. This could be for example
         * <code>"localOffset"</code> for a local variable access instruction. Arguments with the
         * same {@link #getKind()} may have different {@link #getName() names}. A name is typically
         * more descriptive than just the kind and should be preferred over the kind for debug
         * output.
         *
         * @since 24.2
         */
        public String getName() {
            return getDescriptor().getName();
        }

        /**
         * Converts this argument to an <code>int</code> value. This method is only supported if the
         * argument {@link #getKind kind} is {@link Kind#INTEGER}. If called for arguments of other
         * kinds then an {@link UnsupportedOperationException} is thrown.
         *
         * @since 24.2
         */
        public int asInteger() throws UnsupportedOperationException {
            throw unsupported();
        }

        /**
         * Converts this argument to a bytecode index. This method is only supported if the argument
         * {@link #getKind kind} is {@link Kind#BYTECODE_INDEX}. If called for arguments of other
         * kinds then an {@link UnsupportedOperationException} is thrown. If the returned value is
         * >= 0 then the bytecode index can be used to be converted to a {@link BytecodeLocation}.
         *
         * @since 24.2
         */
        public int asBytecodeIndex() {
            throw unsupported();
        }

        /**
         * Converts this argument to a object constant. This method is only supported if the
         * argument {@link #getKind kind} is {@link Kind#CONSTANT}. If called for arguments of other
         * kinds then an {@link UnsupportedOperationException} is thrown.
         *
         * @since 24.2
         */
        public Object asConstant() {
            throw unsupported();
        }

        /**
         * Converts this argument to a {@link Node cached node}. This method is only supported if
         * the argument {@link #getKind kind} is {@link Kind#NODE_PROFILE}. If called for arguments
         * of other kinds then an {@link UnsupportedOperationException} is thrown. The returned
         * value is never <code>null</code> if the {@link BytecodeTier} is
         * {@link BytecodeTier#CACHED}.
         *
         * @since 24.2
         */
        public Node asCachedNode() {
            throw unsupported();
        }

        /**
         * Converts this argument to a {@link TagTreeNode tag tree node}. This method is only
         * supported if the argument {@link #getKind kind} is {@link Kind#TAG_NODE}. If called for
         * arguments of other kinds then an {@link UnsupportedOperationException} is thrown. The
         * returned value is never <code>null</code>.
         *
         * @since 24.2
         */
        public TagTreeNode asTagNode() {
            throw unsupported();
        }

        /**
         * Converts this argument to a local offset. This method is only supported if the argument
         * {@link #getKind kind} is {@link Kind#LOCAL_OFFSET}. If called for arguments of other
         * kinds then an {@link UnsupportedOperationException} is thrown. This index may be used to
         * access locals with the local access methods in {@link BytecodeNode}.
         *
         * @see BytecodeNode#getLocalValue(int, com.oracle.truffle.api.frame.Frame, int)
         * @since 24.2
         */
        public int asLocalOffset() {
            throw unsupported();
        }

        /**
         * Converts this argument to a local index. This method is only supported if the argument
         * {@link #getKind kind} is {@link Kind#LOCAL_INDEX}. If called for arguments of other kinds
         * then an {@link UnsupportedOperationException} is thrown. The local index can be used to
         * index into the list of {@link BytecodeNode#getLocals() locals}.
         *
         * @see BytecodeNode#getLocals()
         * @since 24.2
         */
        public int asLocalIndex() {
            throw unsupported();
        }

        /**
         * Converts this argument to a {@link BranchProfile branch profile}. This method is only
         * supported if the argument {@link #getKind kind} is {@link Kind#BRANCH_PROFILE}. If called
         * for arguments of other kinds then an {@link UnsupportedOperationException} is thrown. The
         * returned value is never <code>null</code>.
         *
         * @since 24.2
         */
        public BranchProfile asBranchProfile() {
            throw unsupported();
        }

        /**
         * Converts this argument to a {@link SpecializationInfo specialization info}. This method
         * is only supported if the argument {@link #getKind kind} is {@link Kind#NODE_PROFILE}. If
         * called for arguments of other kinds then an {@link UnsupportedOperationException} is
         * thrown. The specialization info is only available if
         * {@link GenerateBytecode#enableSpecializationIntrospection()} is set to <code>true</code>.
         *
         * @since 24.2
         */
        public final List<SpecializationInfo> getSpecializationInfo() {
            List<SpecializationInfo> info = getSpecializationInfoInternal();
            if (info != null) {
                return info;
            }

            Node n = asCachedNode();
            if (Introspection.isIntrospectable(n)) {
                try {
                    return Introspection.getSpecializations(n);
                } catch (UnsupportedOperationException o) {
                    // for backwards compatibility fallback to null
                }
            }
            return null;
        }

        /**
         * Allows the generated code to customize the specialization info lookup.
         *
         * @since 25.1
         */
        protected List<SpecializationInfo> getSpecializationInfoInternal() {
            return null;
        }

        private RuntimeException unsupported() {
            return new UnsupportedOperationException(String.format("Not supported for argument type %s.", getKind()));
        }

        /**
         * {@inheritDoc}
         *
         * @since 24.2
         */
        @Override
        public final String toString() {
            switch (getKind()) {
                case LOCAL_OFFSET:
                    return String.format("%s(%d)", getName(), asLocalOffset());
                case LOCAL_INDEX:
                    return String.format("%s(%d)", getName(), asLocalIndex());
                case INTEGER:
                    return String.format("%s(%d)", getName(), asInteger());
                case CONSTANT:
                    return String.format("%s(%s)", getName(), printConstant(asConstant()));
                case NODE_PROFILE:
                    return String.format("%s(%s)", getName(), printNodeProfile(asCachedNode()));
                case BYTECODE_INDEX:
                    return String.format("%s(%04x)", getName(), asBytecodeIndex());
                case BRANCH_PROFILE:
                    return String.format("%s(%s)", getName(), asBranchProfile());
                case TAG_NODE:
                    return String.format("%s%s", getName(), printTagProfile(asTagNode()));
                default:
                    throw new UnsupportedOperationException("Unexpected argument kind " + getKind());
            }
        }

        private static String printTagProfile(TagTreeNode o) {
            if (o == null) {
                return "null";
            }
            return TagTreeNode.format(o);
        }

        private String printNodeProfile(Object o) {
            if (o == null) {
                return "Disabled";
            }
            StringBuilder sb = new StringBuilder();
            sb.append("Enabled");
            List<SpecializationInfo> info = getSpecializationInfo();
            if (info != null) {
                sb.append("(");
                String sep = "";
                boolean empty = true;
                for (SpecializationInfo specialization : info) {
                    if (specialization.getInstances() == 0) {
                        continue;
                    }
                    empty = false;
                    sb.append(sep);
                    sb.append(specialization.getMethodName());
                    sep = "#";
                }
                if (empty) {
                    sb.append("UNINITIALIZED");
                }
                if (o instanceof Node n && !n.isAdoptable()) {
                    sb.append(",SINGLETON");
                }
                sb.append(")");
            }
            return sb.toString();
        }

        private static String printConstant(Object value) {
            if (value == null) {
                return "null";
            }
            if (value instanceof LocalAccessor || value instanceof LocalRangeAccessor) {
                // trusted toString to contain the type name
                return value.toString();
            } else if (value instanceof String || value instanceof TruffleString) {
                return "\"" + truncateString(value.toString()) + "\"";
            } else if (value instanceof Number n) {
                return formatLiteral(n);
            } else {
                String typeString = value.getClass().getSimpleName();
                String valueString = value.getClass().isArray() ? printArray(value, 16) : value.toString();
                return String.format("%s %s", typeString, truncateString(valueString));
            }
        }

        private static String formatLiteral(Number n) {
            String s = n.toString();
            String suffix = "";
            if (n instanceof Float) {
                suffix = "F";
            } else if (n instanceof Long) {
                suffix = "L";
            } else if (n instanceof Double) {
                suffix = "D";
            }
            return s + suffix;
        }

        private static String truncateString(String s) {
            String replaced = s.replace("\n", "\\n");
            if (replaced.length() > 30) {
                replaced = replaced.substring(0, 27) + "...";
            }
            return replaced;
        }

        private static String printArray(Object array, int limit) {
            Object a = array;
            int length = Array.getLength(array);
            if (length > limit) {
                a = truncateArray(array, limit);
            }
            String s;
            if (a instanceof Object[] objArr) {
                s = Arrays.toString(objArr);
            } else if (a instanceof long[] longArr) {
                s = Arrays.toString(longArr);
            } else if (a instanceof int[] intArr) {
                s = Arrays.toString(intArr);
            } else if (a instanceof short[] shortArr) {
                s = Arrays.toString(shortArr);
            } else if (a instanceof char[] charArr) {
                s = Arrays.toString(charArr);
            } else if (a instanceof byte[] byteArr) {
                s = Arrays.toString(byteArr);
            } else if (a instanceof double[] doubleArr) {
                s = Arrays.toString(doubleArr);
            } else if (a instanceof float[] floatArr) {
                s = Arrays.toString(floatArr);
            } else if (a instanceof boolean[] boolArr) {
                s = Arrays.toString(boolArr);
            } else {
                throw new AssertionError(String.format("Unhandled array type %s", array));
            }
            if (length > limit) {
                return "[length:" + length + "]" + s;
            }
            return s;

        }

        private static Object truncateArray(Object array, int limit) {
            if (array instanceof Object[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof long[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof int[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof short[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof char[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof byte[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof double[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof float[] arr) {
                return Arrays.copyOf(arr, limit);
            } else if (array instanceof boolean[] arr) {
                return Arrays.copyOf(arr, limit);
            }
            return array;
        }

        /**
         * Represents the kind of an {@link Argument}.
         *
         * @since 24.2
         */
        public enum Kind {
            /**
             * A constant value. Typically, constants are used to encode {@link ConstantOperand} and
             * LoadConstant builtin operations.
             *
             * @see Argument#asConstant()
             * @since 24.2
             */
            CONSTANT,

            /**
             * A bytecode index. Typically, bytecode indices are used to encode branch targets or
             * the locations of child instructions.
             *
             * @see Argument#asBytecodeIndex()
             * @since 24.2
             */
            BYTECODE_INDEX,

            /**
             * An integer value. Typically, integer arguments are used to encode argument indices
             * and other integer constants.
             *
             * @see Argument#asInteger()
             * @since 24.2
             */
            INTEGER,

            /**
             * The logical frame offset of a local variable. If
             * {@link GenerateBytecode#enableBlockScoping() block scoping} is enabled, multiple
             * locals can share an offset; otherwise, a local's offset is the same as its index.
             * Typically, local offset arguments are used to encode local variable metadata in local
             * variable instructions.
             * <p>
             * The local offset does <i>not</i> represent a frame index that can be used to directly
             * access locals from the frame. Instead, use local accessor instructions,
             * {@link LocalAccessor} operands, or the helper methods defined by the
             * {@link BytecodeNode}.
             *
             * @see Argument#asLocalOffset()
             * @since 24.2
             */
            LOCAL_OFFSET,

            /**
             * The unique index of a local variable. Typically, local index arguments are used to
             * encode local variable metadata in local variable instructions.
             *
             * @see Argument#asLocalIndex()
             * @since 24.2
             */
            LOCAL_INDEX,

            /**
             * A node profile. Typically, node profile arguments are used to encode cached nodes for
             * user-defined {@link Operation operations}.
             *
             * @see Argument#asCachedNode()
             * @since 24.2
             */
            NODE_PROFILE,

            /**
             * A branch profile. Typically, branch profiles are used to profile the outcomes of
             * conditional branch instructions.
             *
             * @see Argument#asBranchProfile()
             * @since 24.2
             */
            BRANCH_PROFILE,
            /**
             * A {@link TagTreeNode}. Typically, tag tree nodes are used to represent nodes in the
             * logical tag tree defined by Tag operations.
             */
            TAG_NODE;
        }

        /**
         * Represents a branch profile.
         *
         * @since 24.2
         */
        @SuppressWarnings("dangling-doc-comments")
        public record BranchProfile(
                        /**
                         * The index of the profile for the branch profile table.
                         *
                         * @since 24.2
                         */
                        int index,

                        /**
                         * The number of times this conditional branch was taken.
                         *
                         * @since 24.2
                         */
                        int trueCount,

                        /**
                         * The number of times this conditional branch was not taken.
                         *
                         * @since 24.2
                         */
                        int falseCount) {

            /**
             * Returns the frequency recorded by this profile.
             *
             * @since 24.2
             */
            public double getFrequency() {
                int total = trueCount + falseCount;
                if (total == 0) {
                    return 0.0d;
                }
                return ((double) trueCount) / ((double) total);
            }

            /**
             * {@inheritDoc}
             *
             * @since 24.2
             */
            @Override
            public String toString() {
                if (trueCount + falseCount == 0) {
                    return index + ":never executed";
                }
                return String.format("%s:%.2f", index, getFrequency());
            }

        }

    }

    static final class InstructionIterable implements Iterable<Instruction> {

        private final BytecodeNode bytecodeNode;

        InstructionIterable(BytecodeNode bytecodeNode) {
            this.bytecodeNode = bytecodeNode;
        }

        @Override
        public Iterator<Instruction> iterator() {
            return new InstructionIterator(bytecodeNode.findInstruction(0));
        }

    }

    private static final class InstructionIterator implements Iterator<Instruction> {

        private Instruction current;

        InstructionIterator(Instruction start) {
            this.current = start;
        }

        public boolean hasNext() {
            return current != null;
        }

        public Instruction next() {
            if (current == null) {
                throw new NoSuchElementException();
            }
            Instruction next = current;
            current = next.next();
            return next;
        }

    }

}
